<!DOCTYPE html>
<html>
<head>
<title>Pipes</title>
<meta name="description" content="An editor for snapping pipes together and moving and rotating them as a single assembly." />
<!-- Copyright 1998-2016 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<link href="../assets/css/goSamples.css" rel="stylesheet" type="text/css" />  <!-- you don't need to use this -->
<script src="goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
<script id="code">
  function init() {
    if (window.goSamples) goSamples();  // init for these samples -- you don't need to call this
    var $ = go.GraphObject.make;  // for more concise visual tree definitions

    myDiagram =
      $(go.Diagram, "myDiagramDiv",
        {
          scale: 1.5,
          "commandHandler.defaultScale": 1.5,
          allowDrop: true,  // accept drops from palette
          allowLink: false,  // no user-drawn links
          initialContentAlignment: go.Spot.Center,
          // use a custom DraggingTool instead of the standard one, defined below
          draggingTool: new SnappingTool(),
          "undoManager.isEnabled": true
        });

    // when the document is modified, add a "*" to the title and enable the "Save" button
    myDiagram.addDiagramListener("Modified", function(e) {
      var button = document.getElementById("SaveButton");
      if (button) button.disabled = !myDiagram.isModified;
      var idx = document.title.indexOf("*");
      if (myDiagram.isModified) {
        if (idx < 0) document.title += "*";
      } else {
        if (idx >= 0) document.title = document.title.substr(0, idx);
      }
    });

    myDiagram.nodeTemplateMap.add("Comment",
      $(go.Node,
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        $(go.TextBlock,
          { stroke: "brown", font: "9pt sans-serif" },
          new go.Binding("text"))
      ));

    // Define the generic "pipe" Node.
    // The Shape gets it Geometry from a geometry path string in the bound data.
    // This node also gets all of its ports from an array of port data in the bound data.
    myDiagram.nodeTemplate =
      $(go.Node, "Spot",
        {
          locationObjectName: "SHAPE",
          locationSpot: go.Spot.Center,
          selectionAdorned: false,  // use a Binding on the Shape.stroke to show selection
          itemTemplate:
            // each port is an "X" shape whose alignment spot and port ID are given by the item data
            $(go.Panel,
              new go.Binding("portId", "id"),
              new go.Binding("alignment", "spot", go.Spot.parse),
              $(go.Shape, "XLine",
                { width: 6, height: 6, background: "transparent", fill: null, stroke: "gray" },
                new go.Binding("figure", "id", portFigure),  // portFigure converter is defined below
                new go.Binding("angle", "angle"))
            ),
          // hide a port when it is connected
          linkConnected: function(node, link, port) {
            port.visible = false;
          },
          linkDisconnected: function(node, link, port) {
            port.visible = true;
          }
        },
        // this creates the variable number of ports for this Spot Panel, based on the data
        new go.Binding("itemArray", "ports"),
        // remember the location of this Node
        new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
        // remember the angle of this Node
        new go.Binding("angle", "angle").makeTwoWay(),
        // move a selected part into the Foreground layer, so it isn't obscured by any non-selected parts
        new go.Binding("layerName", "isSelected", function(s) { return s ? "Foreground" : ""; }).ofObject(),
        $(go.Shape,
          {
            name: "SHAPE",
            // the following are default values;
            // actual values may come from the node data object via data-binding
            geometryString: "F1 M0 0 L20 0 20 20 0 20 z",
            fill: "rgba(128, 128, 128, 0.5)"
          },
          // this determines the actual shape of the Shape
          new go.Binding("geometryString", "geo"),
          // selection causes the stroke to be blue instead of black
          new go.Binding("stroke", "isSelected", function(s) { return s ? "dodgerblue" : "black"; }).ofObject())
      );

    // Show different kinds of port fittings by using different shapes in this Binding converter
    function portFigure(pid) {
      if (pid === null || pid === "") return "XLine";
      if (pid[0] === 'F') return "CircleLine";
      if (pid[0] === 'M') return "PlusLine";
      return "XLine";  // including when the first character is 'U'
    }

    myDiagram.nodeTemplate.contextMenu =
      $(go.Adornment, "Vertical",
        $("ContextMenuButton",
          $(go.TextBlock, "Rotate +45"),
          { click: function(e, obj) { rotate(obj.part.adornedPart, 45); } }),
        $("ContextMenuButton",
          $(go.TextBlock, "Rotate -45"),
          { click: function(e, obj) { rotate(obj.part.adornedPart, -45); } }),
        $("ContextMenuButton",
          $(go.TextBlock, "Rotate 180"),
          { click: function(e, obj) { rotate(obj.part.adornedPart, 180); } }),
        $("ContextMenuButton",
          $(go.TextBlock, "Detach"),
          { click: function(e, obj) { detachSelection(); } }),
        $("ContextMenuButton",
          $(go.TextBlock, "Delete"),
          { click: function(e, obj) { e.diagram.commandHandler.deleteSelection(); } })
      );

    // Change the angle of the parts connected with the given node
    function rotate(node, angle) {
      var tool = myDiagram.toolManager.draggingTool;  // should be a SnappingTool
      myDiagram.startTransaction("rotate " + angle.toString());
      var sel = new go.Set(go.Node);
      sel.add(node);
      var coll = tool.computeEffectiveCollection(sel).toKeySet();
      var bounds = myDiagram.computePartsBounds(coll);
      var center = bounds.center;
      coll.each(function(n) {
        n.angle += angle;
        n.location = n.location.copy().subtract(center).rotate(angle).add(center);
      });
      myDiagram.commitTransaction("rotate " + angle.toString());
    }

    function detachSelection() {
      myDiagram.startTransaction("detach");
      var coll = new go.Set(go.Link);
      myDiagram.selection.each(function(node) {
        if (!(node instanceof go.Node)) return;
        node.linksConnected.each(function(link) {
          // ignore links to other selected nodes
          if (link.getOtherNode(node).isSelected) return;
          // disconnect this link
          coll.add(link);
        });
      });
      myDiagram.removeParts(coll, false);
      myDiagram.commitTransaction("detach");
    }

    // no visual representation of any link data
    myDiagram.linkTemplate = $(go.Link, { visible: false });

    // this model needs to know about particular ports
    myDiagram.model =
      $(go.GraphLinksModel,
        {
          copiesArrays: true, 
          copiesArrayObjects: true,
          linkFromPortIdProperty: "fid",
          linkToPortIdProperty: "tid"
        });

    // Make sure the pipes are ordered by their key in the palette inventory
    function keyCompare(a, b) {
      var at = a.data.key;
      var bt = b.data.key;
      if (at < bt) return -1;
      if (at > bt) return 1;
      return 0;
    }

    myPalette =
      $(go.Palette, "myPaletteDiv",
        {
          scale: 1.2,
          contentAlignment: go.Spot.Center,
          nodeTemplate: myDiagram.nodeTemplate,  // shared with the main Diagram
          "contextMenuTool.isEnabled": false,
          layout: $(go.GridLayout,
                    {
                      cellSize: new go.Size(1, 1), spacing: new go.Size(5, 5),
                      wrappingColumn: 12, comparer: keyCompare
                    }),
          // initialize the Palette with a few "pipe" nodes
          model: $(go.GraphLinksModel,
            {
              copiesArrays: true,
              copiesArrayObjects: true,
              linkFromPortIdProperty: "fid",
              linkToPortIdProperty: "tid",
              nodeDataArray: [
                // Several different kinds of pipe objects, some already rotated for convenience.
                // Each "glue point" is implemented by a port.
                // The port's identifier's first letter must be the type of connector or "fitting".
                // The port's identifier's second letter must be indicate the direction in which a
                // connection may be made: 0-7, indicating multiples of 45-degree angles starting at zero.
                // If you want more than one port of a particular type in the same direction,
                // you will need to add a suffix to the port identifier.
                // The Spot determines the approximate location of the port on the shape.
                // The exact position is offset in order to account for the thickness of the stroke.
                // Each should be shifted towards the center of the shape by the fraction of its
                // distance from the center times the stroke thickness.
                // The following offsets assume the strokeWidth == 1.
                {
                  key: 1,
                  geo: "F1 M0 0 L20 0 20 20 0 20z",
                  ports: [
                    { id: "U6", spot: "0.5 0 0 0.5" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 3, angle: 90,
                  geo: "F1 M0 0 L20 0 20 20 0 20z",
                  ports: [
                    { id: "U6", spot: "0.5 0 0 0.5" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 5,
                  geo: "F1 M0 0 L20 0 20 60 0 60z",
                  ports: [
                    { id: "U6", spot: "0.5 0 0 0.5" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 7, angle: 90,
                  geo: "F1 M0 0 L20 0 20 60 0 60z",
                  ports: [
                    { id: "U6", spot: "0.5 0 0 0.5" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 11,
                  geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U2", spot: "0.25 1 0.25 -0.5" }
                  ]
                },
                {
                  key: 12, angle: 90,
                  geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U2", spot: "0.25 1 0.25 -0.5" }
                  ]
                },
                {
                  key: 13, angle: 180,
                  geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U2", spot: "0.25 1 0.25 -0.5" }
                  ]
                },
                {
                  key: 14, angle: 270,
                  geo: "F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U2", spot: "0.25 1 0.25 -0.5" }
                  ]
                },
                {
                  key: 21,
                  geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U4", spot: "0 0.25 0.5 0.25" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 22, angle: 90,
                  geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U4", spot: "0 0.25 0.5 0.25" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 23, angle: 180,
                  geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U4", spot: "0 0.25 0.5 0.25" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 24, angle: 270,
                  geo: "F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z",
                  ports: [
                    { id: "U0", spot: "1 0.25 -0.5 0.25" },
                    { id: "U4", spot: "0 0.25 0.5 0.25" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 31,
                  geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z",
                  ports: [
                    { id: "U6", spot: "0 0 10.5 0.5" },
                    { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 }
                  ]
                },
                {
                  key: 32, angle: 90,
                  geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z",
                  ports: [
                    { id: "U6", spot: "0 0 10.5 0.5" },
                    { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 }
                  ]
                },
                {
                  key: 33, angle: 180,
                  geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z",
                  ports: [
                    { id: "U6", spot: "0 0 10.5 0.5" },
                    { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 }
                  ]
                },
                {
                  key: 34, angle: 270,
                  geo: "F1 M0 0 L20 0 20 10 Q20 14.142 22.929 17.071 L30 24.142 15.858 38.284 8.787 31.213 Q0 22.426 0 10z",
                  ports: [
                    { id: "U6", spot: "0 0 10.5 0.5" },
                    { id: "U1", spot: "1 1 -7.571 -7.571", angle: 45 }
                  ]
                },
                {
                  key: 41,
                  geo: "F1 M14.142 0 L28.284 14.142 14.142 28.284 0 14.142z",
                  ports: [
                    { id: "U1", spot: "1 1 -7.321 -7.321" },
                    { id: "U3", spot: "0 1 7.321 -7.321" },
                    { id: "U5", spot: "0 0 7.321 7.321" },
                    { id: "U7", spot: "1 0 -7.321 7.321" }
                  ]
                },

                // Example M-F connector pipes
                /*
                {
                  key: 107, //angle: 90,
                  geo: "F1 M0 0 L5 0, 5 10, 15 10, 15 0, 20 0, 20 40, 0 40z",
                  ports: [
                    { id: "F6", spot: "0.5 0 0 10.5" },
                    { id: "U2", spot: "0.5 1 0 -0.5" }
                  ]
                },
                {
                  key: 108, //angle: 90,
                  geo: "F1 M0 0, 20 0, 20 30, 15 30, 15 40, 5 40, 5 30, 0 30z",
                  ports: [
                    { id: "U6", spot: "0.5 0 0 10.5" },
                    { id: "M2", spot: "0.5 1 0 -0.5" }
                  ]
                }
                */

              ]  // end nodeDataArray
            })  // end model
        });  // end Palette

    load();
  } // end init


  // Define a custom DraggingTool
  function SnappingTool() {
    go.DraggingTool.call(this);
  }
  go.Diagram.inherit(SnappingTool, go.DraggingTool);

  // This predicate checks to see if the ports can snap together.
  // The first letter of the port id should be "U", "F", or "M" to indicate which kinds of port may connect.
  // The second letter of the port id should be a digit to indicate which direction it may connect.
  // The ports also need to not already have any link connections and need to face opposite directions.
  SnappingTool.prototype.compatiblePorts = function(p1, p2) {
    // already connected?
    var part1 = p1.part;
    var id1 = p1.portId;
    if (id1 === null || id1 === "") return false;
    if (part1.findLinksConnected(id1).count > 0) return false;
    var part2 = p2.part;
    var id2 = p2.portId;
    if (id2 === null || id2 === "") return false;
    if (part2.findLinksConnected(id2).count > 0) return false;
    // compatible fittings?
    if ((id1[0] === 'U' && id2[0] === 'U') ||
        (id1[0] === 'F' && id2[0] === 'M') ||
        (id1[0] === 'M' && id2[0] === 'F')) {
      // find their effective sides, after rotation
      var a1 = this.effectiveAngle(id1, part1.angle);
      var a2 = this.effectiveAngle(id2, part2.angle);
      // are they in opposite directions?
      if (a1-a2 === 180 || a1-a2 === -180) return true;
    }
    return false;
  };

  // At what angle can a port connect, adjusting for the node's rotation
  SnappingTool.prototype.effectiveAngle = function(id, angle) {
    var dir = id[1];
    var a = 0;
    if (dir === '1') a = 45;
    else if (dir === '2') a = 90;
    else if (dir === '3') a = 135;
    else if (dir === '4') a = 180;
    else if (dir === '5') a = 225;
    else if (dir === '6') a = 270;
    else if (dir === '7') a = 315;
    a += angle;
    if (a < 0) a += 360;
    else if (a >= 360) a -= 360;
    return a;
  };

  // Override this method to find the offset such that a moving port can
  // be snapped to be coincident with a compatible stationary port,
  // then move all of the parts by that offset.
  /** @override */
  SnappingTool.prototype.moveParts = function(parts, offset, check) {
    // when moving an actually copied collection of Parts, use the offset that was calculated during the drag
    if (this._snapOffset && this.isActive && this.diagram.lastInput.up && parts === this.copiedParts) {
      go.DraggingTool.prototype.moveParts.call(this, parts, this._snapOffset, check);
      this._snapOffset = undefined;
      return;
    }

    var commonOffset = offset;

    // find out if any snapping is desired for any Node being dragged
    var sit = parts.iterator;
    while (sit.next()) {
      var node = sit.key;
      if (!(node instanceof go.Node)) continue;
      var info = sit.value;
      var newloc = info.point.copy().add(offset);

      // now calculate snap point for this Node
      var snapoffset = newloc.copy().subtract(node.location);
      var nearbyports = null;
      var closestDistance = 20*20;  // don't bother taking sqrt
      var closestPort = null;
      var closestPortPt = null;
      var nodePort = null;
      var mit = node.ports;
      while (mit.next()) {
        var port = mit.value;
        if (node.findLinksConnected(port.portId).count > 0) continue;
        var portPt = port.getDocumentPoint(go.Spot.Center);
        portPt.add(snapoffset);  // where it would be without snapping

        if (nearbyports === null) {
          // this collects the Nodes that intersect with the NODE's bounds,
          // excluding nodes that are being dragged (i.e. in the PARTS collection)
          var nearbyparts = this.diagram.findObjectsIn(node.actualBounds,
                                    function(x) { return x.part; },
                                    function(p) { return !parts.contains(p); },
                                    true);

          // gather a collection of GraphObjects that are stationary "ports" for this NODE
          nearbyports = new go.Set(go.GraphObject);
          nearbyparts.each(function(n) {
            if (n instanceof go.Node) {
              nearbyports.addAll(n.ports);
            }
          });
        }

        var pit = nearbyports.iterator;
        while (pit.next()) {
          var p = pit.value;
          if (!this.compatiblePorts(port, p)) continue;
          var ppt = p.getDocumentPoint(go.Spot.Center);
          var d = ppt.distanceSquaredPoint(portPt);
          if (d < closestDistance) {
            closestDistance = d;
            closestPort = p;
            closestPortPt = ppt;
            nodePort = port;
          }
        }
      }

      // found something to snap to!
      if (closestPort !== null) {
        // move the node so that the compatible ports coincide
        var noderelpt = nodePort.getDocumentPoint(go.Spot.Center).subtract(node.location);
        var snappt = closestPortPt.copy().subtract(noderelpt);
        // save the offset, to ensure everything moves together
        commonOffset = snappt.subtract(newloc).add(offset);
        // ignore any node.dragComputation function
        // ignore any node.minLocation and node.maxLocation
        break;
      }
    }

    // now do the standard movement with the single (perhaps snapped) offset
    this._snapOffset = commonOffset.copy();  // remember for mouse-up when copying
    go.DraggingTool.prototype.moveParts.call(this, parts, commonOffset, check);
  };

  // Establish links between snapped ports,
  // and remove obsolete links because their ports are no longer coincident.
  /** @override */
  SnappingTool.prototype.doDropOnto = function(pt, obj) {
    go.DraggingTool.prototype.doDropOnto.call(this, pt, obj);
    var tool = this;
    // Need to iterate over all of the dropped nodes to see which ports happen to be snapped to stationary ports
    var coll = this.copiedParts || this.draggedParts;
    var it = coll.iterator;
    while (it.next()) {
      var node = it.key;
      if (!(node instanceof go.Node)) continue;
      // connect all snapped ports of this NODE (yes, there might be more than one) with links
      var pit = node.ports;
      while (pit.next()) {
        var port = pit.value;
        // maybe add a link -- see if the port is at another port that is compatible
        var portPt = port.getDocumentPoint(go.Spot.Center);
        if (!portPt.isReal()) continue;
        var nearbyports =
            this.diagram.findObjectsAt(portPt,
                      function(x) {  // some GraphObject at portPt
                        var o = x;
                        // walk up the chain of panels
                        while (o !== null && o.portId === null) o = o.panel;
                        return o;
                      },
                      function(p) {  // a "port" Panel
                        // the parent Node must not be in the dragged collection, and
                        // this port P must be compatible with the NODE's PORT
                        if (coll.contains(p.part)) return false;
                        var ppt = p.getDocumentPoint(go.Spot.Center);
                        if (portPt.distanceSquaredPoint(ppt) >= 0.25) return false;
                        return tool.compatiblePorts(port, p);
                      });
        // did we find a compatible port?
        var np = nearbyports.first();
        if (np !== null) {
          // connect the NODE's PORT with the other port found at the same point
          this.diagram.toolManager.linkingTool.insertLink(node, port, np.part, np);
        }
      }
    }
  };

  // Just move selected nodes when SHIFT moving, causing nodes to be unsnapped.
  // When SHIFTing, must disconnect all links that connect with nodes not being dragged.
  // Without SHIFT, move all nodes that are snapped to selected nodes, even indirectly.
  /** @override */
  SnappingTool.prototype.computeEffectiveCollection = function(parts) {
    if (this.diagram.lastInput.shift) {
      var links = new go.Set(go.Link);
      var coll = go.DraggingTool.prototype.computeEffectiveCollection.call(this, parts);
      coll.iteratorKeys.each(function(node) {
        // disconnect all links of this node that connect with stationary node
        if (!(node instanceof go.Node)) return;
        node.findLinksConnected().each(function(link) {
          // see if this link connects with a node that is being dragged
          var othernode = link.getOtherNode(node);
          if (othernode !== null && !coll.contains(othernode)) {
            links.add(link);  // remember for later deletion
          }
        });
      });
      // outside of nested loops we can actually delete the links
      links.each(function(l) { l.diagram.remove(l); });
      return coll;
    } else {
      var map = new go.Map(go.Part, Object);
      if (parts === null) return map;
      var tool = this;
      parts.iterator.each(function(n) {
        tool.gatherConnecteds(map, n);
      });
      return map;
    }
  };

  // Find other attached nodes.
  SnappingTool.prototype.gatherConnecteds = function(map, node) {
    if (!(node instanceof go.Node)) return;
    if (map.contains(node)) return;
    // record the original Node location, for relative positioning and for cancellation
    map.add(node, { point: node.location });
    // now recursively collect all connected Nodes and the Links to them
    var tool = this;
    node.findLinksConnected().each(function(link) {
      map.add(link, { point: new go.Point() });
      tool.gatherConnecteds(map, link.getOtherNode(node));
    });
  };
  // end SnappingTool class

  function save() {
    document.getElementById("mySavedModel").value = myDiagram.model.toJson();
    myDiagram.isModified = false;
  }
  function load() {
    myDiagram.model = go.Model.fromJson(document.getElementById("mySavedModel").value);
  }
</script>

</head>
<body onload="init()">
<div id="sample">
  <div id="myPaletteDiv" style="border: solid 1px gray; width:100%; height:160px"></div>
  <div id="myDiagramDiv" style="border: solid 1px gray; width:100%; height:500px"></div>
  <div id ="description">
  <p>
    Nodes in this sample use <a>Shape.geometryString</a> to determine their shape. 
    You can see more custom geometry examples and read about geometryString
    on the <a href="../intro/geometry.html">Geometry Path Strings Introduction page.</a>
  </p>
  <p>
    As a part's unconnected port (shown by an X) comes close to a stationary port
    with which it is compatible, the dragged selection snaps so that those ports coincide.
    A custom <a>DraggingTool</a>, called <b>SnappingTool</b>, is used to check compatibility. 
  </p>
  <p>
    Dragging automatically drags all connected parts.
    Hold down the Shift key before dragging in order to detach a part from the parts it is connected with.
    These functionalities are also controlled by the custom SnappingTool.
  </p>
  <p>
    Use the <a>GraphObject.contextMenu</a> to rotate, detach, or delete a node.
    If it is connected with other parts, the whole collection rotates.
  </p>
  </div>
  <div>
    <div>
      <button id="SaveButton" onclick="save()">Save</button>
      <button onclick="load()">Load</button>
      Diagram Model saved in JSON format:
    </div>
    <textarea id="mySavedModel" style="width:100%;height:300px">
{ "class": "go.GraphLinksModel",
  "copiesArrays": true,
  "copiesArrayObjects": true,
  "linkFromPortIdProperty": "fid",
  "linkToPortIdProperty": "tid",
  "nodeDataArray": [
{"key":0, "category":"Comment", "text":"Use Shift to disconnect a shape", "loc":"0 -13"},
{"key":1, "category":"Comment", "text":"The Context Menu has more commands", "loc":"0 20"},
{"key":2, "category":"Comment", "text":"Gray Xs are unconnected ports", "loc":"0 -47"},
{"key":3, "category":"Comment", "text":"Dragged Shapes snap to unconnected ports", "loc":"0 -80"},
{"key":11, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-187.33333333333331 -69.33333333333331", "angle":0},
{"key":12, "angle":90, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-147.33333333333331 -69.33333333333331"},
{"key":21, "geo":"F1 M0 0 L60 0 60 20 50 20 Q40 20 40 30 L40 40 20 40 20 30 Q20 20 10 20 L0 20z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U4", "spot":"0 0.25 0.5 0.25"},{"id":"U2", "spot":"0.5 1 0 -0.5"} ], "loc":"-137.33333333333331 -9.333333333333314", "angle":0},
{"key":5, "geo":"F1 M0 0 L20 0 20 60 0 60z", "ports":[ {"id":"U6", "spot":"0.5 0 0 0.5"},{"id":"U2", "spot":"0.5 1 0 -0.5"} ], "loc":"-197.33333333333331 -19.333333333333314", "angle":0},
{"key":13, "angle":180, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-147.33333333333331 30.666666666666685"},
{"key":14, "angle":270, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-187.33333333333331 30.666666666666685"},
{"key":-7, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-76.66666666666663 -8.666666666666657", "angle":0},
{"key":-8, "angle":90, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-36.66666666666663 -8.666666666666657"},
{"key":-9, "angle":180, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-36.66666666666663 31.333333333333343"},
{"key":-10, "angle":270, "geo":"F1 M0 40 L0 30 Q0 0 30 0 L40 0 40 20 30 20 Q20 20 20 30 L20 40z", "ports":[ {"id":"U0", "spot":"1 0.25 -0.5 0.25"},{"id":"U2", "spot":"0.25 1 0.25 -0.5"} ], "loc":"-76.66666666666663 31.333333333333343"}
],
  "linkDataArray": [
{"from":12, "to":11, "fid":"U2", "tid":"U0"},
{"from":5, "to":11, "fid":"U6", "tid":"U2"},
{"from":13, "to":21, "fid":"U2", "tid":"U2"},
{"from":14, "to":5, "fid":"U0", "tid":"U2"},
{"from":13, "to":14, "fid":"U0", "tid":"U2"},
{"from":-8, "to":-7, "fid":"U2", "tid":"U0"},
{"from":-9, "to":-8, "fid":"U2", "tid":"U0"},
{"from":-10, "to":-7, "fid":"U0", "tid":"U2"},
{"from":-10, "to":-9, "fid":"U2", "tid":"U0"}
]}
    </textarea>
  </div>
</div>
</body>
</html>
